package forge;
import arcane.ui.util.ImageUtil;
import com.google.common.base.Function;
import com.google.common.collect.ComputationException;
import com.google.common.collect.MapMaker;
import com.mortennobel.imagescaling.ResampleOp;
import forge.properties.ForgeProps;
import forge.properties.NewConstants;
import java.awt.*;
import java.awt.geom.AffineTransform;
import java.awt.image.BufferedImage;
import java.io.File;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import static java.lang.Double.parseDouble;
import static java.lang.Math.min;
/**
* This class stores ALL card images in a cache with soft values. this means that the images may be collected when
* they are not needed any more, but will be kept as long as possible.
* <p/>
* The keys are the following:
* <ul>
* <li>Keys start with the file name, extension is skipped</li>
* <li>The key without suffix belongs to the unmodified image from the file</li>
* <li>If the key belongs to a token, "#Token" is appended</li>
* <li>If the key belongs to the unrotated image, "#Normal" is appended</li>
* <li>If the key belongs to the rotated image, "#Tapped" is appended</li>
* <li>If the key belongs to the large preview image, "#<i>scale</i>" is appended, where scale is a double
* precision floating point number</li>
* </ul>
*
* @author Forge
* @version $Id: $
*/
public class ImageCache implements NewConstants {
/** Constant <code>imageCache</code> */
private static final Map<String, BufferedImage> imageCache;
/** Constant <code>FULL_SIZE</code> */
private static final Pattern FULL_SIZE = Pattern.compile("(.*)#(\\d+.\\d+)");
/** Constant <code>TOKEN="#Token"</code> */
/** Constant <code>NORMAL="#Normal"</code> */
/** Constant <code>TAPPED="#Tapped"</code> */
/** Constant <code>NORMAL="#Normal"</code> */
/** Constant <code>TAPPED="#Tapped"</code> */
private static final String TOKEN = "#Token", NORMAL = "#Normal", TAPPED = "#Tapped";
/** Constant <code>scaleLargerThanOriginal=true</code> */
public static boolean scaleLargerThanOriginal = true;
static {
imageCache = new MapMaker().softValues().makeComputingMap(new Function<String, BufferedImage>() {
public BufferedImage apply(String key) {
try {
//DEBUG
/*System.out.printf("Currently %d %s in the cache%n", imageCache.size(),
imageCache.size() == 1? "image":"images");*/
//DEBUG
//System.out.printf("New Image for key: %s%n", key);
if (key.endsWith(NORMAL)) {
//normal
key = key.substring(0, key.length() - NORMAL.length());
return getNormalSizeImage(imageCache.get(key));
} else if (key.endsWith(TAPPED)) {
//tapped
key = key.substring(0, key.length() - TAPPED.length());
return getTappedSizeImage(imageCache.get(key));
}
Matcher m = FULL_SIZE.matcher(key);
if (m.matches()) {
//full size
key = m.group(1);
return getFullSizeImage(imageCache.get(key), parseDouble(m.group(2)));
} else {
//original
File path;
if (key.endsWith(TOKEN)) {
key = key.substring(0, key.length() - TOKEN.length());
path = ForgeProps.getFile(IMAGE_TOKEN);
} else path = ForgeProps.getFile(IMAGE_BASE);
File file = null;
file = new File(path, key + ".jpg");
if (!file.exists()) {
//DEBUG
//System.out.println("File not found, no image created: " + file);
return null;
}
BufferedImage image = ImageUtil.getImage(file);
return image;
}
} catch (Exception ex) {
//DEBUG
//System.err.println("Exception, no image created");
if (ex instanceof ComputationException) throw (ComputationException) ex;
else throw new ComputationException(ex);
}
}
});
}
/**
* Returns the image appropriate to display the card in a zone
*
* @param card a {@link forge.Card} object.
* @return a {@link java.awt.image.BufferedImage} object.
*/
public static BufferedImage getImage(Card card) {
String key = card.isFaceDown() ? "Morph" : getKey(card);
if (card.isTapped()) key += TAPPED;
else key += NORMAL;
return getImage(key);
}
/**
* Returns the image appropriate to display the card in the picture panel
*
* @param card a {@link forge.Card} object.
* @param width a int.
* @param height a int.
* @return a {@link java.awt.image.BufferedImage} object.
*/
public static BufferedImage getImage(Card card, int width, int height) {
String key = (card.isFaceDown() && card.getController().isComputer()) ? "Morph" : getKey(card);
BufferedImage original = getImage(key);
if (original == null) return null;
double scale = min((double) width / original.getWidth(), (double) height / original.getHeight());
//here would be the place to limit the scaling, scaling option in menu ?
if (scale > 1 && !scaleLargerThanOriginal) scale = 1;
return getImage(key + "#" + scale);
}
/**
* <p>getOriginalImage.</p>
*
* @param card a {@link forge.Card} object.
* @return a {@link java.awt.image.BufferedImage} object.
*/
public static BufferedImage getOriginalImage(Card card) {
String key = (card.isFaceDown() && card.getController().isComputer()) ? "Morph" : getKey(card);
return getImage(key);
}
/**
* Returns the Image corresponding to the key
*
* @param key a {@link java.lang.String} object.
* @return a {@link java.awt.image.BufferedImage} object.
*/
private static BufferedImage getImage(String key) {
try {
BufferedImage image = imageCache.get(key);
//if an image is still cached and it was not the expected size, drop it
if (!isExpectedSize(key, image)) {
imageCache.remove(key);
image = imageCache.get(key);
}
return image;
} catch (NullPointerException ex) {
//unfortunately NullOutputException, thrown when apply() returns null, is not public
//NullOutputException is a subclass of NullPointerException
//legitimate, happens when a card has no image
return null;
} catch (ComputationException ex) {
if (ex.getCause() instanceof NullPointerException) return null;
ex.printStackTrace();
return null;
}
}
/**
* Returns if the image for the key is the proper size.
*
* @param key a {@link java.lang.String} object.
* @param image a {@link java.awt.image.BufferedImage} object.
* @return a boolean.
*/
private static boolean isExpectedSize(String key, BufferedImage image) {
if (key.endsWith(NORMAL)) {
//normal
return image.getWidth() == Constant.Runtime.width[0]
&& image.getHeight() == Constant.Runtime.height[0];
} else if (key.endsWith(TAPPED)) {
//tapped
return image.getWidth() == Constant.Runtime.height[0]
&& image.getHeight() == Constant.Runtime.width[0];
} else {
//original & full is never wrong
return true;
}
}
/**
* Returns the map key for a card, without any suffixes for the image size.
*
* @param card a {@link forge.Card} object.
* @return a {@link java.lang.String} object.
*/
private static String getKey(Card card) {
/* String key = GuiDisplayUtil.cleanString(card.getImageName());
//if(card.isBasicLand() && card.getRandomPicture() != 0) key += card.getRandomPicture();
File path = null;
String tkn = "";
if (card.isToken() && !card.isCopiedToken())
{
path = ForgeProps.getFile(IMAGE_TOKEN);
tkn = TOKEN;
}
else
path = ForgeProps.getFile(IMAGE_BASE);
File f = null;
if (!card.getCurSetCode().equals(""))
{
String nn = "";
if (card.getRandomPicture() > 0)
nn = Integer.toString(card.getRandomPicture() + 1);
StringBuilder sbKey = new StringBuilder();
//First try 3 letter set code with MWS filename format
sbKey.append(card.getCurSetCode() + "/");
sbKey.append(GuiDisplayUtil.cleanStringMWS(card.getName()) + nn + ".full");
f = new File(path, sbKey.toString() + ".jpg");
if (f.exists())
return sbKey.toString();
sbKey = new StringBuilder();
//Second, try 2 letter set code with MWS filename format
sbKey.append(SetInfoUtil.getSetCode2_SetCode3(card.getCurSetCode()) + "/");
sbKey.append(GuiDisplayUtil.cleanStringMWS(card.getName()) + nn + ".full");
f = new File(path, sbKey.toString() + ".jpg");
if (f.exists())
return sbKey.toString();
sbKey = new StringBuilder();
//Third, try 3 letter set code with Forge filename format
sbKey.append(card.getCurSetCode() + "/");
sbKey.append(GuiDisplayUtil.cleanString(card.getName()) + nn);
f = new File(path, sbKey.toString() + ".jpg");
if (f.exists())
return sbKey.toString();
//Last, give up with set images, go with the old picture type
f = new File(path, key + nn + ".jpg");
if (f.exists())
return key;
//if still no file, download if option enabled
}
int n = card.getRandomPicture();
if (n > 0)
key += n;
key += tkn;
// key = GuiDisplayUtil.cleanString(key);
*/
if (card.isToken() && !card.isCopiedToken())
return GuiDisplayUtil.cleanString(card.getImageName()) + TOKEN;
return card.getImageFilename(); //key;
}
/**
* Returns an image scaled to the size given in {@link Constant.Runtime}
*
* @param original a {@link java.awt.image.BufferedImage} object.
* @return a {@link java.awt.image.BufferedImage} object.
*/
private static BufferedImage getNormalSizeImage(BufferedImage original) {
int srcWidth = original.getWidth();
int srcHeight = original.getHeight();
int tgtWidth = Constant.Runtime.width[0];
int tgtHeight = Constant.Runtime.height[0];
if (srcWidth == tgtWidth && srcHeight == tgtHeight) return original;
// AffineTransform at = new AffineTransform();
// at.scale((double) tgtWidth / srcWidth, (double) tgtHeight / srcHeight);
//// at.translate(srcHeight, 0);
//// at.rotate(PI / 2);
// double scale = min((double) tgtWidth / srcWidth, (double) tgtHeight / srcHeight);
//
// BufferedImage image = new BufferedImage(tgtWidth, tgtHeight, BufferedImage.TYPE_INT_ARGB);
// Graphics2D g2d = (Graphics2D) image.getGraphics();
// g2d.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BICUBIC);
// g2d.drawImage(scale < 0.5? ImageUtil.getBlurredImage(original, 6, 1.0f):original, at, null);
// g2d.dispose();
ResampleOp resampleOp = new ResampleOp(tgtWidth, tgtHeight); //defaults to Lanczos3
BufferedImage image = resampleOp.filter(original, null);
return image;
}
/**
* Returns an image scaled to the size given in {@link Constant.Runtime}, but rotated
*
* @param original a {@link java.awt.image.BufferedImage} object.
* @return a {@link java.awt.image.BufferedImage} object.
*/
private static BufferedImage getTappedSizeImage(BufferedImage original) {
/*int srcWidth = original.getWidth();
int srcHeight = original.getHeight();*/
int tgtWidth = Constant.Runtime.width[0];
int tgtHeight = Constant.Runtime.height[0];
AffineTransform at = new AffineTransform();
// at.scale((double) tgtWidth / srcWidth, (double) tgtHeight / srcHeight);
at.translate(tgtHeight, 0);
at.rotate(Math.PI / 2);
//
// double scale = min((double) tgtWidth / srcWidth, (double) tgtHeight / srcHeight);
//
// BufferedImage image = new BufferedImage(tgtHeight, tgtWidth, BufferedImage.TYPE_INT_ARGB);
// Graphics2D g2d = (Graphics2D) image.getGraphics();
// g2d.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BICUBIC);
// g2d.drawImage(scale < 0.5? ImageUtil.getBlurredImage(original, 6, 1.0f):original, at, null);
// g2d.dispose();
ResampleOp resampleOp = new ResampleOp(tgtWidth, tgtHeight); //defaults to Lanczos3
BufferedImage image = resampleOp.filter(original, null);
BufferedImage rotatedImage = new BufferedImage(tgtHeight, tgtWidth, BufferedImage.TYPE_INT_ARGB);
Graphics2D g2d = (Graphics2D) rotatedImage.getGraphics();
g2d.drawImage(image, at, null);
g2d.dispose();
return rotatedImage;
}
/**
* Returns an image scaled to the size appropriate for the card picture panel
*
* @param original a {@link java.awt.image.BufferedImage} object.
* @param scale a double.
* @return a {@link java.awt.image.BufferedImage} object.
*/
private static BufferedImage getFullSizeImage(BufferedImage original, double scale) {
if (scale == 1) return original;
// AffineTransform at = new AffineTransform();
// at.scale(scale, scale);
//
// BufferedImage image = new BufferedImage((int) (original.getWidth() * scale),
// (int) (original.getHeight() * scale), BufferedImage.TYPE_INT_ARGB);
// Graphics2D g2d = (Graphics2D) image.getGraphics();
// g2d.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BICUBIC);
// g2d.drawImage(scale < 0.5? ImageUtil.getBlurredImage(original, 6, 1.0f):original, at, null);
// g2d.dispose();
ResampleOp resampleOp = new ResampleOp((int) (original.getWidth() * scale),
(int) (original.getHeight() * scale)); //defaults to Lanczos3
BufferedImage image = resampleOp.filter(original, null);
return image;
}
/**
* <p>clear.</p>
*/
public static void clear() {
imageCache.clear();
}
}